package solver

import (
	"fmt"
	"gosudoku/set"
	"log"
	"strings"
)

func (g *Grid) XYWing() (used bool) {
	for _, row := range g.rows {
		for _, pivot := range row.cells {
			if g.XYWingCell(pivot, 2) {
				used = true
			}
		}
	}
	return
}

func (g *Grid) XYZWing() (used bool) {
	for _, row := range g.rows {
		for _, pivot := range row.cells {
			if g.XYWingCell(pivot, 3) {
				used = true
			}
		}
	}
	return
}

func (g *Grid) XYWingCell(pivot *Cell, CandCount int) (used bool) {
	if pivot.cands.Count() != CandCount {
		return
	}
	for _, wings := range g.getXYWings(pivot) {
		ok, cand := verifyXYWing(pivot, wings)
		if !ok {
			continue
		}
		if g.clearupWings(cand, pivot, wings) {
			used = true
			logXYWing(cand, pivot, wings)
			return
		}
	}
	return
}

func verifyXYWing(pivot *Cell, wings []*Cell) (ok bool, cand int) {
	if sameHouse(wings) {
		return
	}
	p := pivot.cands
	w1 := wings[0].cands
	w2 := wings[1].cands
	z := w1.Intersect(w2)
	if z.Count() != 1 {
		return
	}
	if p.Count() == 2 && p != w1.SymDiff(w2) {
		return
	}
	if p.Count() == 3 && p != w1.Union(w2) {
		return
	}
	ok = true
	cand = z.GetList()[0]
	return
}

func sameHouse(cs []*Cell) bool {
	rowNums := set.NewSet()
	colNums := set.NewSet()
	boxNums := set.NewSet()
	for _, c := range cs {
		rowNums.Add(c.rowNum)
		colNums.Add(c.colNum)
		boxNums.Add(c.boxNum)
	}
	return rowNums.Count() == 1 || colNums.Count() == 1 || boxNums.Count() == 1

}

func (g *Grid) getXYWings(pivot *Cell) (allWings [][]*Cell) {
	seenCells := g.seenCells(pivot)
	cells := make([]*Cell, 0)
	for _, seenCell := range seenCells {
		if seenCell.cands.Count() == 2 {
			cells = append(cells, seenCell)
		}
	}
	nums := make([]int, 0, len(cells))
	for i := range cells {
		nums = append(nums, i)
	}
	for _, pair := range combine(nums, 2) {
		allWings = append(allWings, []*Cell{cells[pair[0]], cells[pair[1]]})
	}
	return
}

func (g *Grid) clearupWings(cand int, pivot *Cell, wings []*Cell) (used bool) {
	/*
		if !wings[0].cands.Has(cand) {
			wings[0], wings[1] = wings[1], wings[0]
		}*/
	for i, wing := range wings {
		if wing.cands.Has(cand) {
			wings[0], wings[i] = wings[i], wings[0]
			break
		}
	}
	relCells := g.seenCells(wings[0])
	for _, wing := range wings[1:] {
		if !wing.cands.Has(cand) {
			continue
		}
		relCells = shareCells(relCells, g.seenCells(wing))
	}

	if pivot.cands.Count() == len(wings)+1 {
		relCells = shareCells(relCells, g.seenCells(pivot))
	}

	for _, cell := range relCells {
		if cell.Equal(pivot) {
			continue
		}
		if cell.removeCand(cand) {
			used = true
		}
	}
	return
}

func logXYWing(cand int, pivot *Cell, wings []*Cell) {
	var b strings.Builder
	var sname string
	if len(wings) == 3 {
		sname = "WXYZ-Wing"
	} else if len(wings) == 4 {
		sname = "VWXYZ-Wing"
	} else if pivot.cands.Count() == 2 {
		sname = "XY-Wing"
	} else {
		sname = "XYZ-Wing"
	}
	fmt.Fprintf(&b, "pr%vc%v ", pivot.rowNum, pivot.colNum)
	for _, wing := range wings {
		fmt.Fprintf(&b, "wr%vc%v ", wing.rowNum, wing.colNum)
	}
	fmt.Fprintf(&b, "%v %v\n", toValue(cand), sname)
	log.Print(b.String())
}

//WXYZ wing
/*
There are 4 cells that are the pivot and 3 wings.
There are 4 candidates that belong to the 4 cells.

If two wings can see each other, their candidates' union should be xyz.
This means many options:
1. xz, yz. (this is the standard type)
2. xyz, xyz.
3. xz, xyz.
4. xy, xyz.

*/
func (g *Grid) WXYZWing() (used bool) {
	for _, row := range g.rows {
		for _, pivot := range row.cells {
			if g.WXYZWingCell(pivot) {
				used = true
			}
		}
	}
	return
}

func (g *Grid) WXYZWingCell(pivot *Cell) (used bool) {
	if pivot.cands.Count() != 3 && pivot.cands.Count() != 4 {
		return
	}

	for _, wings := range g.getWXYZWings(pivot) {
		ok, cand := verifyWXYZWing(pivot, wings)
		if !ok {
			continue
		}
		if g.clearupWings(cand, pivot, wings) {
			used = true
			logXYWing(cand, pivot, wings)
			return
		}
	}
	return
}

func (g *Grid) getWXYZWings(pivot *Cell) (allWings [][]*Cell) {
	seenCells := g.seenCells(pivot)
	cells := make([]*Cell, 0)
	for _, seenCell := range seenCells {
		if seenCell.cands.Count() == 2 || seenCell.cands.Count() == 3 {
			cells = append(cells, seenCell)
		}
	}
	nums := make([]int, 0, len(cells))
	for i := range cells {
		nums = append(nums, i)
	}
	for _, pair := range combine(nums, 3) {
		allWings = append(allWings, []*Cell{cells[pair[0]], cells[pair[1]], cells[pair[2]]})
	}
	return
}

func verifyWXYZWing(pivot *Cell, wings []*Cell) (used bool, cand int) {
	if sameHouse(wings) {
		return
	}

	yes, wing1, wing2, wing3 := twoWingsSameHouse(wings)
	p := pivot.cands
	wxyz := p.Union(wing1.cands).Union(wing2.cands).Union(wing3.cands)
	if wxyz.Count() != 4 {
		return
	}
	var z set.Set
	if yes {
		xyz := wing1.cands.Union(wing2.cands)
		if xyz.Count() != 3 {
			return
		}
		wz := wing3.cands
		if wz.Count() != 2 {
			return
		}
		z = wz.Intersect(xyz)
		if z.Count() != 1 {
			return
		}
	} else {
		xz, yz, wz := wing1.cands, wing2.cands, wing3.cands
		if xz.Count() != 2 || yz.Count() != 2 || yz.Count() != 2 {
			return
		}
		z = xz.Intersect(yz).Intersect(wz)
		if z.Count() != 1 {
			return
		}
	}

	if p != wxyz && p != wxyz.Diff(z) {
		return
	}

	used = true
	cand = z.GetList()[0]
	return
}

func twoWingsSameHouse(wings []*Cell) (yes bool, w1 *Cell, w2 *Cell, w3 *Cell) {
	w1, w2, w3 = wings[0], wings[1], wings[2]
	if sameHouse([]*Cell{wings[0], wings[1]}) {
		yes = true
	} else if sameHouse([]*Cell{wings[1], wings[2]}) {
		w1, w2 = wings[1], wings[2]
		w3 = wings[0]
		yes = true
	} else if sameHouse([]*Cell{wings[0], wings[2]}) {
		w1, w2 = wings[0], wings[2]
		w3 = wings[1]
		yes = true
	}

	return
}

func (g *Grid) WWing() (used bool) {
	return g.doStrategy(g.wWingHouse)
}

func (g *Grid) wWingHouse(house House) (used bool) {
	for cand := 0; cand < 9; cand++ {
		if g.wWingCand(house, cand) {
			used = true
		}
	}
	return
}

func (g *Grid) wWingCand(house House, cand int) (used bool) {
	cells := chooseHasCandCells(house, cand)
	if len(cells) != 2 {
		return
	}

	wings1 := filterWWingEnds(cand, g.seenCells(cells[0]))
	wings2 := filterWWingEnds(cand, g.seenCells(cells[1]))

	for _, wing1 := range wings1 {
		if wing1 == cells[1] {
			continue
		}
		for _, wing2 := range wings2 {
			if wing2 == cells[0] {
				continue
			}
			if wing1 == wing2 || sameHouse([]*Cell{wing1, wing2}) {
				continue
			}
			if wing1.cands != wing2.cands {
				continue
			}
			removedCand := wing1.cands.GetList()[0]
			if removedCand == cand {
				removedCand = wing1.cands.GetList()[1]
			}
			if g.clearupWWing(removedCand, cells, []*Cell{wing1, wing2}) {
				used = true
				logWWing(cells, []*Cell{wing1, wing2}, removedCand)
			}
		}
	}

	return
}

func filterWWingEnds(cand int, cells []*Cell) (endCells []*Cell) {
	for _, cell := range cells {
		if cell.cands.Count() != 2 || !cell.cands.Has(cand) {
			continue
		}
		endCells = append(endCells, cell)
	}
	return
}

func logWWing(cells []*Cell, wings []*Cell, removedCand int) {
	var b strings.Builder
	fmt.Fprintf(&b, "pr%vc%v pr%vc%v ", cells[0].rowNum, cells[0].colNum, cells[1].rowNum, cells[1].colNum)
	fmt.Fprintf(&b, "wr%vc%v wr%vc%v ", wings[0].rowNum, wings[0].colNum, wings[1].rowNum, wings[1].colNum)
	fmt.Fprintf(&b, "%v W-Wing", toValue(removedCand))
	log.Println(b.String())
}

func (g *Grid) clearupWWing(cand int, cells []*Cell, wings []*Cell) (used bool) {
	relCells := g.seenCells(wings[0])
	for _, cell := range wings[1:] {
		relCells = shareCells(relCells, g.seenCells(cell))
	}

	for _, cell := range relCells {
		if cell.isMemberOf(cells) {
			continue
		}
		if cell.removeCand(cand) {
			used = true
		}
	}
	return
}

func (g *Grid) VWXYZWing() (used bool) {
	for _, row := range g.rows {
		for _, pivot := range row.cells {
			if g.VWXYZWingCell(pivot) {
				return true
			}
		}
	}
	return
}

func (g *Grid) VWXYZWingCell(pivot *Cell) (used bool) {
	if pivot.cands.Count() != 4 && pivot.cands.Count() != 5 {
		return
	}
	for _, wings := range g.getVWXYZWings(pivot) {
		ok, cand := verifyVWXYZWing(pivot, wings)
		if !ok {
			continue
		}
		if g.clearupWings(cand, pivot, wings) {
			used = true
			logXYWing(cand, pivot, wings)
			return
		}
	}
	return
}

func (g *Grid) getVWXYZWings(pivot *Cell) (allWings [][]*Cell) {
	seenCells := g.seenCells(pivot)
	cells := make([]*Cell, 0)
	for _, seenCell := range seenCells {
		if seenCell.cands.Count() >= 2 && seenCell.cands.Count() <= 4 {
			cells = append(cells, seenCell)
		}
	}
	nums := make([]int, 0, len(cells))
	for i := range cells {
		nums = append(nums, i)
	}
	for _, subset := range combine(nums, 4) {
		allWings = append(allWings, []*Cell{cells[subset[0]], cells[subset[1]], cells[subset[2]], cells[subset[3]]})
	}
	return
}

func verifyVWXYZWing(pivot *Cell, wings []*Cell) (used bool, cand int) {
	if sameHouse(wings) {
		return
	}

	allCands := getAllCands(wings)
	if allCands.Count() != 5 {
		return
	}

	wingGroups := divideWingGroups(wings)
	z := getAllCands(wingGroups[0])
	for _, group := range wingGroups[1:] {
		cands := getAllCands(group)
		if cands.Count() != len(group)+1 {
			return
		}
		z = z.Intersect(cands)
	}
	if z.Count() != 1 {
		return
	}

	if allCands != pivot.cands && allCands.Diff(z) != pivot.cands {
		return
	}

	used = true
	cand = z.GetList()[0]
	return
}

func divideWingGroups(wings []*Cell) (wingGroups [][]*Cell) {
	nums := make([]int, 0, len(wings))
	for i := range wings {
		nums = append(nums, i)
	}
	for _, subset := range combine(nums, 3) {
		group := []*Cell{wings[subset[0]], wings[subset[1]], wings[subset[2]]}
		if !sameHouse(group) {
			continue
		}
		wingGroups = append(wingGroups, group)
		last := set.NewSet(nums).Diff(set.NewSet(subset)).GetList()[0]
		wingGroups = append(wingGroups, []*Cell{wings[last]})
		return
	}
	for _, subset := range combine(nums, 2) {
		group := []*Cell{wings[subset[0]], wings[subset[1]]}
		if !sameHouse(group) {
			continue
		}
		wingGroups = append(wingGroups, group)

		lasts := set.NewSet(nums).Diff(set.NewSet(subset)).GetList()
		group = []*Cell{wings[lasts[0]], wings[lasts[1]]}
		if sameHouse(group) {
			wingGroups = append(wingGroups, group)
			return
		} else {
			wingGroups = append(wingGroups, []*Cell{wings[lasts[0]]})
			wingGroups = append(wingGroups, []*Cell{wings[lasts[1]]})
			return
		}
	}

	wingGroups = append(wingGroups, []*Cell{wings[0]})
	wingGroups = append(wingGroups, []*Cell{wings[1]})
	wingGroups = append(wingGroups, []*Cell{wings[2]})
	wingGroups = append(wingGroups, []*Cell{wings[3]})
	return
}

func getAllCands(wings []*Cell) (allCands set.Set) {
	allCands = wings[0].cands
	for _, wing := range wings[1:] {
		allCands = allCands.Union(wing.cands)
	}
	return
}
